package com.zmc.lostfound.post.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpStatus;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.zmc.lostfound.common.constant.PostConstant;
import com.zmc.lostfound.common.dto.SimpleEmailDTO;
import com.zmc.lostfound.common.dto.UserBaseInfoDTO;
import com.zmc.lostfound.common.exception.MyException;
import com.zmc.lostfound.common.feign.AuthFeign;
import com.zmc.lostfound.common.feign.ThirdPartyFeign;
import com.zmc.lostfound.common.utils.Result;
import com.zmc.lostfound.common.utils.redis.RedisCacheUtil;
import com.zmc.lostfound.post.dao.PostDao;
import com.zmc.lostfound.post.entity.PostEntity;
import com.zmc.lostfound.post.entity.PostImageEntity;
import com.zmc.lostfound.post.service.*;
import com.zmc.lostfound.post.vo.PostReviewSaveVO;
import com.zmc.lostfound.post.vo.post.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import static com.zmc.lostfound.common.constant.RabbitMqConstant.*;
import static com.zmc.lostfound.common.constant.RedisConstant.*;

/**
 * @author ZMC
 * @description 帖子管理业务接口
 * @email 2869581855@qq.com
 * @date 2023-07-05 15:49:44
 */
@Service
public class PostServiceImpl extends ServiceImpl<PostDao, PostEntity> implements PostService {

    private final PostImageService postImageService;

    private final PostReviewService postReviewService;

    private final PostReportService postReportService;

    private final PostStarService postStarService;

    private final CommentService commentService;

    private final AuthFeign authFeign;

    private final ThirdPartyFeign thirdPartyFeign;

    private final RedisCacheUtil redisCacheUtil;

    private final StringRedisTemplate stringRedisTemplate;

    private final ThreadPoolExecutor threadPoolExecutor;

    private final RabbitTemplate rabbitTemplate;

    public PostServiceImpl(PostImageService postImageService, PostReviewService postReviewService, PostReportService postReportService, PostStarService postStarService, CommentService commentService, AuthFeign authFeign, ThirdPartyFeign thirdPartyFeign, RedisCacheUtil redisCacheUtil, StringRedisTemplate stringRedisTemplate, ThreadPoolExecutor threadPoolExecutor, RabbitTemplate rabbitTemplate) {
        this.postImageService = postImageService;
        this.postReviewService = postReviewService;
        this.postReportService = postReportService;
        this.postStarService = postStarService;
        this.commentService = commentService;
        this.authFeign = authFeign;
        this.thirdPartyFeign = thirdPartyFeign;
        this.redisCacheUtil = redisCacheUtil;
        this.stringRedisTemplate = stringRedisTemplate;
        this.threadPoolExecutor = threadPoolExecutor;
        this.rabbitTemplate = rabbitTemplate;
    }

    /**
     * 按帖子id查询帖子详细信息
     *
     * @param postId 帖子id
     * @return PostDetailInfoDTO
     */
    @Override
    public PostDetailInfoVO queryDetailInfoByPostId(Long postId) {
        if (postId == null || postId <= 0) {
            MyException.throwException("查询失败: 帖子id不能为null且必须大于0");
        }
        // 1.查询缓存
        PostDetailInfoVO postDetailInfoVO;
        String key = CACHE_POST_DETAIL_KEY_PREFIX + postId;
        Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(key);
        boolean notEmpty = CollectionUtil.isNotEmpty(map);
        boolean notBlank = StrUtil.isNotBlank((String) map.get("id"));
        // 1.1缓存命中
        if (notEmpty && notBlank) {
            postDetailInfoVO = JSONUtil.toBean(JSONUtil.toJsonStr(map), PostDetailInfoVO.class);
            Object fromUser = map.get("fromUser");
            if (fromUser != null) {
                postDetailInfoVO.setFromUser(JSONUtil.toBean(JSONUtil.toJsonStr(fromUser), FromUser.class));
            }
            Object postImageList = map.get("postImageList");
            if (postImageList != null) {
                postDetailInfoVO.setPostImageList(JSONUtil.toList(JSONUtil.toJsonStr(postImageList), PostImage.class));
            }
            return postDetailInfoVO;
        }
        // 1.2缓存命中"空对象", 表示数据库中不存在
        if (notEmpty) {
            return null;
        }
        // 2.缓存未命中，数据库中可能存在
        PostEntity postEntity = this.getById(postId);
        if (postEntity == null) {
            // 2.1数据库中不存在，利用缓存空对象的思想，避免缓存穿透
            Map<String, Object> postDetailInfoMap = new HashMap<>(1);
            postDetailInfoMap.put("id", "");
            redisCacheUtil.setMapWithTTL(key, postDetailInfoMap, CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        // 2.2异步查询帖子的发布用户信息
        CompletableFuture<FromUser> fromUserFuture = CompletableFuture.supplyAsync(() -> {
            Result result = authFeign.queryBaseInfoById(postEntity.getFromUserId());
            if (result == null || HttpStatus.HTTP_OK != result.getCode()) {
                MyException.throwException("查询失败: 无法获取帖子的发布用户信息");
            }
            UserBaseInfoDTO userBaseInfoVO = result.getBean("userBaseInfoVO", UserBaseInfoDTO.class);
            return BeanUtil.copyProperties(userBaseInfoVO, FromUser.class);
        }, threadPoolExecutor);
        // 2.3异步查询帖子的图片信息
        CompletableFuture<List<PostImage>> postImageFuture = CompletableFuture.supplyAsync(() -> {
            List<PostImageEntity> postImageEntityList = postImageService.queryInfoByPostId(postId);
            if (CollectionUtil.isNotEmpty(postImageEntityList)) {
                return postImageEntityList.stream()
                        .map(postImageEntity -> BeanUtil.copyProperties(postImageEntity, PostImage.class))
                        .collect(Collectors.toList());
            }
            return null;
        }, threadPoolExecutor);
        // 2.4封装帖子详情信息
        postDetailInfoVO = BeanUtil.copyProperties(postEntity, PostDetailInfoVO.class);
        postDetailInfoVO.setFromUser(fromUserFuture.join());
        postDetailInfoVO.setPostImageList(postImageFuture.join());
        // 2.5异步添加帖子详细信息到缓存，并设置随机TTL，避免缓存雪崩
        PostDetailInfoVO finalPostDetailInfoVO = postDetailInfoVO;
        fromUserFuture.runAfterBothAsync(postImageFuture, () -> {
            Map<String, Object> postDetailInfoMap = BeanUtil.beanToMap(finalPostDetailInfoVO, new HashMap<>(20),
                    CopyOptions.create()
                            .setIgnoreNullValue(true)
                            .setFieldValueEditor((fieldName, fieldValue) -> {
                                if (fieldValue == null) {
                                    return null;
                                }
                                if ("fromUser".equals(fieldName) || "postImageList".equals(fieldName)) {
                                    return JSONUtil.toJsonStr(fieldValue);
                                }
                                return fieldValue.toString();
                            }));
            redisCacheUtil.setMapWithTTL(key, postDetailInfoMap, CACHE_POST_DETAIL_KEY_PREFIX_TTL, TimeUnit.SECONDS, true);
        }, threadPoolExecutor);
        // 2.6返回帖子信息
        return postDetailInfoVO;
    }

    /**
     * 用户发布帖子
     *
     * @param postSaveVO 用户发布/修改帖子信息请求体
     * @return Result
     */
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public Result publish(PostSaveVO postSaveVO) {
        // 1.获取帖子基本信息
        PostEntity postEntity = BeanUtil.copyProperties(postSaveVO, PostEntity.class);
        // 2.设置帖子的完成状态为 未完成
        postEntity.setCompleteStatus(PostConstant.PostCompleteEnum.NOT_COMPLETE.getCode());
        // 3.设置帖子的审核状态为 未审核
        postEntity.setReviewStatus(PostConstant.PostReviewEnum.NOT_REVIEW.getCode());
        // 4.新增帖子信息
        if (!this.save(postEntity)) {
            MyException.throwException("帖子发布失败");
        }
        // 5.批量新增帖子图片信息
        CompletableFuture<Void> postFuture = CompletableFuture.runAsync(
                () -> postImageService.insertBatchWithPostId(postEntity.getId(), postSaveVO.getImgUrlList())
                , threadPoolExecutor);
        // 6.新增帖子审核记录
        CompletableFuture<Void> postReviewFuture = CompletableFuture.runAsync(() -> {
            PostReviewSaveVO postReviewSaveVO = PostReviewSaveVO.builder()
                    .postId(postEntity.getId())
                    .reviewStatus(PostConstant.PostReviewEnum.NOT_REVIEW.getCode())
                    .build();
            postReviewService.insertPostReview(postReviewSaveVO);
        }, threadPoolExecutor);
        // 7.发送消息到post.topic交换机，实现MySQL与ES的数据同步
        CompletableFuture.allOf(postFuture, postReviewFuture).join();
        CompletableFuture.runAsync(() -> rabbitTemplate.convertAndSend(POST_EXCHANGE, POST_SAVE_KEY, postEntity.getId()), threadPoolExecutor);
        return Result.ok("帖子发布成功");
    }

    /**
     * 用户修改帖子信息
     *
     * @param postSaveVO 用户发布/修改帖子信息请求体
     * @return Result
     */
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public Result updatePost(PostSaveVO postSaveVO) {
        // 1.修改帖子基本信息
        CompletableFuture<Void> postFuture = CompletableFuture.runAsync(
                () -> this.updateById(BeanUtil.copyProperties(postSaveVO, PostEntity.class))
                , threadPoolExecutor);
        // 2.批量修改帖子的图片信息
        CompletableFuture<Void> postImageFuture = CompletableFuture.runAsync(
                () -> postImageService.updateBatchByPostId(postSaveVO.getId(), postSaveVO.getImgUrlList())
                , threadPoolExecutor);
        // 3.新增/修改帖子审核记录
        CompletableFuture<Void> postReviewFuture = CompletableFuture.runAsync(() -> {
            // 3.1查询帖子信息
            PostEntity postEntity = this.query().select("review_status").eq("id", postSaveVO.getId()).one();
            if (postEntity == null) {
                MyException.throwException("修改失败: 帖子信息不存在");
            }
            Integer reviewStatus = postEntity.getReviewStatus();
            /*
             * 如果帖子是 未审核状态，什么都不做
             * 如果帖子是 审核未通过 或 审核通过状态，新增一条该帖子的审核记录，并标记帖子状态为 未审核
             */
            if (!reviewStatus.equals(PostConstant.PostReviewEnum.NOT_REVIEW.getCode())) {
                // 3.2修改帖子的审核状态为 未审核
                CompletableFuture.runAsync(() -> this.updateById(PostEntity.builder()
                        .id(postSaveVO.getId())
                        .reviewStatus(PostConstant.PostReviewEnum.NOT_REVIEW.getCode())
                        .build()), threadPoolExecutor);
                // 3.3新增一条该帖子的审核记录，设置为 未审核
                CompletableFuture.runAsync(() -> postReviewService.insertPostReview(PostReviewSaveVO.builder()
                        .postId(postSaveVO.getId())
                        .reviewStatus(PostConstant.PostReviewEnum.NOT_REVIEW.getCode())
                        .build()), threadPoolExecutor);
            }
        }, threadPoolExecutor);
        // 4.等待postFuture、postImageFuture、postReviewFuture执行成功
        CompletableFuture.allOf(postFuture, postImageFuture, postReviewFuture).join();
        CompletableFuture.runAsync(() -> {
            // 4.1删除redis缓存
            redisCacheUtil.deleteByKey(CACHE_POST_DETAIL_KEY_PREFIX + postSaveVO.getId());
            // 4.2发送消息到post.topic交换机，实现MySQL与ES的数据同步
            rabbitTemplate.convertAndSend(POST_EXCHANGE, POST_SAVE_KEY, postSaveVO.getId());
        }, threadPoolExecutor);
        return Result.ok("帖子信息修改成功");
    }

    /**
     * 用户完成帖子
     *
     * @param postCompleteVO 用户完成帖子请求体
     * @return Result
     */
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public Result complete(PostCompleteVO postCompleteVO) {
        // 1.接口幂等性: 校验该帖子是否已经被完成
        PostDetailInfoVO postDetailInfoVO = this.queryDetailInfoByPostId(postCompleteVO.getPostId());
        if (postDetailInfoVO == null) {
            MyException.throwException("完成失败: 该帖子不存在");
        }
        if (PostConstant.PostCompleteEnum.COMPLETE.getCode().equals(postDetailInfoVO.getCompleteStatus())) {
            MyException.throwException("该帖子已完成，请勿重复操作");
        }
        if (!PostConstant.PostReviewEnum.REVIEW_SUCCESS.getCode().equals(postDetailInfoVO.getReviewStatus())) {
            MyException.throwException("完成失败: 该帖子审核未通过");
        }
        // 2.异步查询帖子的发布用户信息
        CompletableFuture<UserBaseInfoDTO> fromUserFuture = CompletableFuture.supplyAsync(() -> {
            Result result = authFeign.queryBaseInfoById(postDetailInfoVO.getFromUser().getId());
            if (result == null || HttpStatus.HTTP_OK != result.getCode()) {
                MyException.throwException("查询失败: 无法获取帖子的发布用户信息");
            }
            return result.getBean("userBaseInfoVO", UserBaseInfoDTO.class);
        }, threadPoolExecutor);
        // 3.异步查询帖子的完成用户信息
        CompletableFuture<UserBaseInfoDTO> toUserFuture = CompletableFuture.supplyAsync(() -> {
            Result result = authFeign.queryBaseInfoById(postCompleteVO.getToUserId());
            if (result == null || HttpStatus.HTTP_OK != result.getCode()) {
                MyException.throwException("查询失败: 无法获取帖子的完成用户信息");
            }
            return result.getBean("userBaseInfoVO", UserBaseInfoDTO.class);
        }, threadPoolExecutor);
        // 4.异步更新帖子的 完成状态 与 寻物/认领人id
        CompletableFuture.runAsync(() -> {
            // 4.1更新MySQL
            this.updateById(PostEntity.builder()
                    .id(postCompleteVO.getPostId())
                    .completeStatus(PostConstant.PostCompleteEnum.COMPLETE.getCode())
                    .toUserId(postCompleteVO.getToUserId())
                    .build());
            // 4.2.删除redis缓存
            redisCacheUtil.deleteByKey(CACHE_POST_DETAIL_KEY_PREFIX + postCompleteVO.getPostId());
            // 4.2.发送消息到post.topic交换机，实现MySQL与ES的数据同步
            rabbitTemplate.convertAndSend(POST_EXCHANGE, POST_SAVE_KEY, postCompleteVO.getPostId());
        }, threadPoolExecutor);
        // 5.发送QQ邮件，通知用户失物招领信息
        UserBaseInfoDTO fromUser = fromUserFuture.join();
        UserBaseInfoDTO toUser = toUserFuture.join();
        Integer type = postDetailInfoVO.getType();
        if (PostConstant.PostTypeEnum.LOST.getCode().equals(type)) {
            // 5.1完成失物帖: 即有人(完成用户)找到了失物帖上的物品，需要将该用户的联系方式通过QQ邮件发送给失主(发帖用户)，让失主去联系他
            CompletableFuture.runAsync(() -> {
                SimpleEmailDTO simpleEmailDTO = new SimpleEmailDTO();
                simpleEmailDTO.setQqEmail(fromUser.getQqEmail());
                simpleEmailDTO.setTitle("【失物招领平台】---失物找回通知");
                simpleEmailDTO.setText("尊敬的用户，您好！您丢失的物品已经找到了！\n" +
                        "拾取人的信息如下: \n" +
                        "昵称: " + toUser.getNickname() + "，\n" +
                        "联系电话: " + toUser.getPhone() + "，\n" +
                        "联系QQ: " + toUser.getQqEmail() + "，\n" +
                        "找回时间: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "，\n" +
                        "请主动与拾取人联系，拾取人也可能会联系您！"
                );
                // 5.2发送邮件
                thirdPartyFeign.sendSimpleEmail(simpleEmailDTO);
            }, threadPoolExecutor);
            return Result.ok("帖子已完成: 您的联系方式已通过QQ邮件发送给失主，\n" +
                    "您可以在 \"我完成的帖子\" 界面查询发布该帖子的失主联系方式，\n" +
                    "请主动与失主联系，失主也可能会联系您！");
        } else if (PostConstant.PostTypeEnum.FOUND.getCode().equals(type)) {
            // 5.3完成招领帖: 即有失主(完成用户)发现招领贴上的物品是自己的，需要将失主的联系方式通过QQ邮件发送给发帖用户，让发帖用户去联系失主
            CompletableFuture.runAsync(() -> {
                SimpleEmailDTO simpleEmailDTO = new SimpleEmailDTO();
                simpleEmailDTO.setQqEmail(fromUser.getQqEmail());
                simpleEmailDTO.setTitle("【失物招领平台】---失物认领通知");
                simpleEmailDTO.setText("尊敬的用户，您好！您捡到的物品已经被认领了！\n" +
                        "失主的信息如下: \n" +
                        "昵称: " + toUser.getNickname() + "，\n" +
                        "联系电话: " + toUser.getPhone() + "，\n" +
                        "联系QQ: " + toUser.getQqEmail() + "，\n" +
                        "认领时间: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "，\n" +
                        "失主会尽快与您联系，您也可以主动联系失主！"
                );
                // 5.4发送邮件
                thirdPartyFeign.sendSimpleEmail(simpleEmailDTO);
            }, threadPoolExecutor);
            return Result.ok("帖子已完成: 您的联系方式已通过QQ邮件发送给拾取人，\n" +
                    "您可以在 \"我完成的帖子\" 界面查询发布该帖子的拾取人联系方式，\n" +
                    "请主动与拾取人联系，拾取人也可能会联系您！");
        }
        MyException.throwException("完成失败: 未知异常");
        return Result.error("完成失败: 未知异常");
    }

    /**
     * 按帖子id删除帖子
     *
     * @param postId 帖子id
     * @return Result
     */
    @Transactional(rollbackFor = {Exception.class})
    @Override
    public Result deleteByPostId(Long postId) {
        // 1.删除该帖子
        if (!this.removeById(postId)) {
            MyException.throwException("该帖子已删除，请勿重复操作");
        }
        // 2.删除帖子图片信息
        CompletableFuture<Void> postImageFuture = CompletableFuture.runAsync(() -> postImageService.deleteBatchByPostId(postId), threadPoolExecutor);
        // 3.删除帖子的评论信息
        CompletableFuture<Void> commentFuture = CompletableFuture.runAsync(() -> commentService.deleteBatchByPostId(postId), threadPoolExecutor);
        // 4.删除帖子审核信息
        CompletableFuture<Void> postReviewFuture = CompletableFuture.runAsync(() -> postReviewService.deleteBatchByPostId(postId), threadPoolExecutor);
        // 5.删除帖子举报信息
        CompletableFuture<Void> postReportFuture = CompletableFuture.runAsync(() -> postReportService.deleteBatchByPostId(postId), threadPoolExecutor);
        // 6.删除用户&帖子收藏关联信息
        CompletableFuture<Void> postStarFuture = CompletableFuture.runAsync(() -> postStarService.deleteByPostIdAndUserId(postId, null), threadPoolExecutor);
        CompletableFuture.allOf(postImageFuture, commentFuture, postReviewFuture, postReportFuture, postStarFuture).join();
        CompletableFuture.runAsync(() -> {
            // 7.删除redis缓存
            redisCacheUtil.deleteByKey(CACHE_POST_DETAIL_KEY_PREFIX + postId);
            // 8.发送消息到post.topic交换机，实现MySQL与ES的数据同步
            rabbitTemplate.convertAndSend(POST_EXCHANGE, POST_DELETE_KEY, postId);
        }, threadPoolExecutor);
        return Result.ok("删除成功");
    }

}